package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/gogf/gf/util/gconv"
)

var (
	once           sync.Once
	cacheSingleton *Cache
	lock           sync.Mutex
)

type Cache struct {
	mu        sync.RWMutex
	CacheData map[string]*CacheItem
}

// 缓存数据项
type CacheItem struct {
	data   interface{} //数据内容
	expire int64       //过期时间
}

// new singleton cache
func NewSingleCache(ctx context.Context) (res *Cache) {
	once.Do(func() {
		cacheSingleton = &Cache{
			mu:        sync.RWMutex{},
			CacheData: map[string]*CacheItem{},
		}
		go cacheSingleton.gcLoop(ctx)
	})

	return cacheSingleton
}

// 定时清理过期的数据
func (c *Cache) gcLoop(ctx context.Context) {
	ticker := time.NewTicker(time.Millisecond * 20)
	for {
		select {
		case <-ticker.C:
			go c.gcExpireData()
		case <-ctx.Done():
			return
		}
	}
}

// 清理过期的数据
func (c *Cache) gcExpireData() {
	c.mu.Lock()
	defer c.mu.Unlock()

	now := time.Now().Unix()
	for k, v := range c.CacheData {
		if v == nil {
			continue
		}
		// 清理有过期时间的数据
		if v.expire > 0 && now > v.expire {
			delete(c.CacheData, k)
		}
	}
}

// set data
// expire:0 is not expired
func (c *Cache) Set(key string, value interface{}, expire time.Duration) (err error) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if len(key) == 0 {
		return fmt.Errorf("key is not allow empty string")
	}
	if value == nil {
		return fmt.Errorf("value is not allow nil")
	}

	var expireTimeStamp int64
	if expire > 0 {
		expireTimeStamp = time.Now().Add(expire).Unix()
	}
	c.CacheData[key] = &CacheItem{
		data:   value,
		expire: expireTimeStamp,
	}
	return nil
}

// get data
func (c *Cache) Get(key string) (res interface{}) {
	c.mu.RLock()
	defer c.mu.RUnlock()

	if value, has := c.CacheData[key]; has {
		return value.data
	} else {
		return nil
	}
}

// is key contain
func (c *Cache) Contain(key string) (res bool) {
	value := c.Get(key)
	if value != nil {
		return true
	}

	return
}

// del key
func (c *Cache) Remove(key string) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if _, has := c.CacheData[key]; has {
		delete(c.CacheData, key)
	}
}

// get cache keys
func (c *Cache) Keys() (res []string) {
	c.mu.RLock()
	defer c.mu.RUnlock()

	res = make([]string, 0)
	if len(c.CacheData) == 0 {
		return
	}
	for k, _ := range c.CacheData {
		res = append(res, k)
	}

	return res
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	done1 := make(chan bool)
	done2 := make(chan bool)

	go demo1(ctx, done1)
	go demo2(ctx, done2)

	<-done2
	<-done1
	fmt.Println("end")
}

func demo1(ctx context.Context, ch chan bool) {
	c := NewSingleCache(ctx)

	// set cache
	c.Set("foo", "bar", 0)
	c.Set("foo1", "bar1", 0)
	c.Set("foo", 10, 0)                   //cover key
	c.Set("expire_key", "1", time.Second) //set expire key

	// get cache
	val := c.Get("foo")
	fmt.Printf("[Demo1] [c.Get] key:(%s),val:%v\n", "foo", val)

	// contain key
	has := c.Contain("foo2")
	fmt.Printf("[Demo1] [c.Contain] key:(%s),contain:(%v)\n", "foo2", has)

	// del key
	c.Remove("foo1")

	time.Sleep(time.Second * 2)
	// test expire
	keys := c.Keys() // output:[Demo1] [c.Keys] keys:([foo counter])
	fmt.Printf("[Demo1] [c.Keys] keys:(%+v)\n", keys)

	ch <- true
}

func demo2(ctx context.Context, ch chan bool) {
	var (
		total, total1, total2 = 100000, 100000, 100000
		key                   = "counter"
		wg                    = sync.WaitGroup{}
	)

	// test1
	for i := 0; i < total; i++ {
		wg.Add(1)
		go func(wg *sync.WaitGroup) {
			defer wg.Done()
			//lock.Lock()
			//defer lock.Unlock()
			total1 -= 1
		}(&wg)
	}

	// test2
	c := NewSingleCache(ctx)
	c.Set(key, total2, 0)
	for i := 0; i < total; i++ {
		wg.Add(1)
		go func(wg *sync.WaitGroup) {
			lock.Lock()
			defer lock.Unlock()
			defer wg.Done()

			res := c.Get(key)
			val := gconv.Int(res) - 1
			c.Set(key, val, 0)
		}(&wg)
	}

	wg.Wait()
	fmt.Printf("total1 origin:%d res:%d\n", total, total1)
	fmt.Printf("total2 origin:%d res:%d\n", total, c.Get(key))
	ch <- true
}
